package org.lc.oauth.extension.password;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjUtil;
import lombok.extern.slf4j.Slf4j;
import org.lc.oauth.util.OAuth2AuthenticationProviderUtils;
import org.lc.oauth.util.OAuth2EndpointUtils;
import org.lc.oauth.vo.LcUserDetails;
import org.lc.platform.base.constant.AuthConstant;
import org.lc.platform.base.enums.AuthResultEnum;
import org.lc.platform.base.enums.LoginResultEnum;
import org.lc.platform.boot.util.LcWebUtils;
import org.lc.platform.redis.service.CacheService;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.util.CollectionUtils;

import java.security.Principal;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 密码模式身份验证提供者
 * <p>
 * 处理基于用户名和密码的身份验证
 *
 * @author haoxr
 * @since 3.0.0
 */
@Slf4j
public class PasswordAuthenticationProvider implements AuthenticationProvider {
    private final AuthenticationManager authenticationManager;
    private final OAuth2AuthorizationService authorizationService;
    private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;

    /**
     * Constructs an {@code OAuth2ResourceOwnerPasswordAuthenticationProviderNew} using the provided parameters.
     *
     * @param authenticationManager the authentication manager
     * @param authorizationService  the authorization service
     * @param tokenGenerator        the token generator
     * @since 0.2.3
     */
    public PasswordAuthenticationProvider(AuthenticationManager authenticationManager, OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
        Assert.notNull(authorizationService, "authorizationService cannot be null");
        Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
        this.authenticationManager = authenticationManager;
        this.authorizationService = authorizationService;
        this.tokenGenerator = tokenGenerator;
    }


    /*
     * 判断当前验证方式是否是密码授权模式
     * */
    private void isPassWordGrantType(RegisteredClient registeredClient) {
        if (ObjUtil.isNotNull(registeredClient)) {
            var grantType = registeredClient.getAuthorizationGrantTypes();
            if (!grantType.contains(PasswordAuthenticationToken.PASSWORD)) {
                throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
            }
        }
    }


    /*
     * 获取相关认证信息
     * 返回带有权限的认证信息
     */
    private Authentication getUsernamePasswordAuthentication(Map<String, Object> additionalParameters) {
        // 生成用户名密码身份验证令牌
        String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME);
        String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD);

        var usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
        /* 用户名密码身份验证，成功后返回带有权限的认证信息 */
        Authentication usernamePasswordAuthentication = null;
        try {
            usernamePasswordAuthentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
        } catch (Exception e) {
            /* 需要将其他类型的异常转换为 OAuth2AuthenticationException 才能被自定义异常捕获处理 */
            var errMsg = e.getCause() != null ? e.getCause().getMessage() : e.getMessage();
            if (ObjUtil.isNotNull(errMsg)) {
                throw new OAuth2AuthenticationException(e.getCause() != null ? e.getCause().getMessage() : e.getMessage());
            }
            OAuth2EndpointUtils.throwAuthError(LoginResultEnum.FEGIN_ABNORMAL);

        }
        return usernamePasswordAuthentication;
    }


    /*
     * 获取授权范围
     * （暂未支持）
     */
    private Set<String> getAuthorizedScopes(Set<String> requestedScopes, RegisteredClient registeredClient) {
        Set<String> authorizedScopes = null;
        if (registeredClient != null) {
            authorizedScopes = registeredClient.getScopes();
            if (!CollectionUtils.isEmpty(requestedScopes)) {
                Set<String> unauthorizedScopes = requestedScopes.stream().filter(requestedScope -> !registeredClient.getScopes().contains(requestedScope)).collect(Collectors.toSet());

                if (!CollectionUtils.isEmpty(unauthorizedScopes)) {
                    throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
                }
                authorizedScopes = new LinkedHashSet<>(requestedScopes);
            }
        }
        return authorizedScopes;
    }

    /*
     * 获取ACCESS_TOKEN
     */
    private OAuth2AccessToken getAccessToken(DefaultOAuth2TokenContext.Builder tokenBuilder, OAuth2Authorization.Builder authorizationBuilder) {
        var tokenContext = tokenBuilder.tokenType((OAuth2TokenType.ACCESS_TOKEN)).build();
        var generatedAccessToken = this.tokenGenerator.generate(tokenContext);
        if (ObjUtil.isNull(generatedAccessToken)) {
            OAuth2EndpointUtils.throwError(AuthResultEnum.ACCESS_TOKEN_ERROR);
        }
        var accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
        if (generatedAccessToken instanceof ClaimAccessor) {
            authorizationBuilder.token(accessToken, (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()));
        } else {
            authorizationBuilder.accessToken(accessToken);
        }
        return accessToken;
    }

    /*
     * 生成刷新令牌(Refresh Token)
     */
    private OAuth2RefreshToken getRefreshToken(DefaultOAuth2TokenContext.Builder tokenBuilder, OAuth2Authorization.Builder authorizationBuilder) {
        OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build());
        if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
            OAuth2EndpointUtils.throwError(AuthResultEnum.REFRESH_TOKEN_ERROR);
        }
        OAuth2RefreshToken refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
        authorizationBuilder.refreshToken(refreshToken);
        return refreshToken;
    }

    /*
     * 持久化登录
     */
    private void saveAuthentication(OAuth2Authorization.Builder authorizationBuilder, Authentication auth, String token) {
        OAuth2Authorization authorization = authorizationBuilder.build();
        this.authorizationService.save(authorization);
    }


    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        var passwordAuthenticationToken = (PasswordAuthenticationToken) authentication;
        Map<String, Object> additionalParameters = passwordAuthenticationToken.getAdditionalParameters();
        /*   <---------------'获取客户端登录信息，和注册信息'------------------->   */
        var clientPrincipal = OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient(passwordAuthenticationToken);
        var registeredClient = clientPrincipal.getRegisteredClient();
        /* 是否支持刷新token */
        var isSupportRefreshToken = ObjUtil.isNotNull(registeredClient) && registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) && !clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE);

        /* 验证grant_type 是否是密码授权模式 */
        this.isPassWordGrantType(registeredClient);

        /* 验证申请访问范围(Scope) */
        var authorizedScopes = this.getAuthorizedScopes(passwordAuthenticationToken.getScopes(), registeredClient);
        /* 获取登录认证成功地授权信息 */
        var principal = this.getUsernamePasswordAuthentication(additionalParameters);
        /* 访问令牌(Access Token) 构造器 */
        var tokenContextBuilder = DefaultOAuth2TokenContext.builder().registeredClient(registeredClient)
                /* 身份验证成功的认证信息(用户名、权限等信息) */.principal(principal).authorizationServerContext(AuthorizationServerContextHolder.getContext()).authorizedScopes(authorizedScopes)
                /* 授权方式 */.authorizationGrantType(PasswordAuthenticationToken.PASSWORD)
                /* 授权具体对象 */.authorizationGrant(passwordAuthenticationToken);


        var authorizationBuilder = OAuth2Authorization.withRegisteredClient(Objects.requireNonNull(registeredClient)).principalName(principal.getName()).authorizationGrantType(PasswordAuthenticationToken.PASSWORD).authorizedScopes(authorizedScopes).attribute(Principal.class.getName(), principal);
        var accessToken = getAccessToken(tokenContextBuilder, authorizationBuilder);
        boolean recordRefresh = Boolean.parseBoolean(LcWebUtils.getRequest().getParameter("refresh"));
        /* 生成刷新令牌(Refresh Token) */
        OAuth2RefreshToken refreshToken = null;
        if (recordRefresh) {
            if (isSupportRefreshToken) {
                refreshToken = this.getRefreshToken(tokenContextBuilder, authorizationBuilder);
            }
        }
        /* 持久化令牌发放记录到数据库，存储信息到redis */
        this.saveAuthentication(authorizationBuilder, principal, accessToken.getTokenValue());
        /* 此处可添加额外参数（第四个参数）,暂时添加userID 便于相关缓存 */
        var user = (LcUserDetails) principal.getPrincipal();
        if (!user.isSuperAdmin() && user.getAcls().isEmpty()) {
            OAuth2EndpointUtils.throwAuthError(LoginResultEnum.NO_AUTH);
        }
        Map<String, Object> userMap = new HashMap<>(3);
        userMap.put(AuthConstant.USER_ID, user.getUserId());
        userMap.put(AuthConstant.APIS, user.getApis());
        userMap.put(AuthConstant.ACLS, user.getAcls());
        return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, userMap);
    }

    /**
     * 判断传入的 authentication 类型是否与当前认证提供者(AuthenticationProvider)相匹配--模板方法
     * <p>
     * ProviderManager#authenticate 遍历 providers 找到支持对应认证请求的 provider-迭代器模式
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return PasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

}
